/*
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dragoncon.dcsched.io;

import com.dragoncon.dcsched.provider.ScheduleContract;
import com.dragoncon.dcsched.provider.ScheduleContract.Sessions;
import com.dragoncon.dcsched.provider.ScheduleContract.SyncColumns;
import com.dragoncon.dcsched.provider.ScheduleDatabase.SessionsSpeakers;
import com.dragoncon.dcsched.provider.ScheduleDatabase.SessionsTracks;
import com.dragoncon.dcsched.provider.ScheduleDatabase.Tables;
import com.dragoncon.dcsched.util.Lists;
import com.dragoncon.dcsched.util.ParserUtils;
import com.dragoncon.dcsched.util.SpreadsheetEntry;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.text.format.Time;
import android.util.Log;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import static com.dragoncon.dcsched.util.ParserUtils.AtomTags.ENTRY;
import static com.dragoncon.dcsched.util.ParserUtils.sanitizeId;
import static com.dragoncon.dcsched.util.ParserUtils.splitComma;
import static com.dragoncon.dcsched.util.ParserUtils.translateTrackIdAlias;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;

/**
 * Handle a remote {@link XmlPullParser} that defines a set of {@link Sessions}
 * entries. Assumes that the remote source is a Google Spreadsheet.
 */
public class RemoteSessionsHandler extends XmlHandler {
    private static final String TAG = "SessionsHandler";

    /**
     * Custom format used internally that matches expected concatenation of
     * {@link Columns#SESSION_DATE} and {@link Columns#SESSION_TIME}.
     */
    private static final SimpleDateFormat sTimeFormat = new SimpleDateFormat(
            "M/d/yyyy H:mm", Locale.US);
    private static final SimpleDateFormat sTimeSpanFormat = new SimpleDateFormat(
            "h:mm a", Locale.US);
    private static final TimeZone sTZ = TimeZone.getTimeZone("America/New_York");

    public RemoteSessionsHandler() {
        super(ScheduleContract.CONTENT_AUTHORITY);
	sTimeFormat.setTimeZone(sTZ);
	sTimeSpanFormat.setTimeZone(sTZ);
    }

    /** {@inheritDoc} */
    @Override
    public ArrayList<ContentProviderOperation> parse(XmlPullParser parser, ContentResolver resolver)
	throws XmlPullParserException, IOException, RemoteException, OperationApplicationException {
        final ArrayList<ContentProviderOperation> batch = Lists.newArrayList();
	final ArrayList<ContentValues> bulkSessions = Lists.newArrayList();
	final ArrayList<ContentValues> bulkSesTracks = Lists.newArrayList();
	final ArrayList<ContentValues> bulkSesSpeakers = Lists.newArrayList();

	/* This doesn't look like it ought to work, but for inserts, the URI
	   (including session ID) is only used to figure out what table to use
	   and the values inserted must include the session ID, so we can fake
	   the session id to make the URI match us up with the right table.
	*/
	final Uri sessionTracksUri = Sessions.buildTracksDirUri("x");
	final Uri sessionSpeakersUri = Sessions.buildSpeakersDirUri("x");

        // Walk document, parsing any incoming entries
        int type;
        while ((type = parser.next()) != END_DOCUMENT) {
            if (type == START_TAG && ENTRY.equals(parser.getName())) {
                // Process single spreadsheet row at a time
                final SpreadsheetEntry entry = SpreadsheetEntry.fromParser(parser);
                final long serverUpdated = entry.getUpdated();

                final String sessionId = sanitizeId(entry.get(Columns.SESSION_TITLE));

                // Check for existing details, only update when changed
                final Uri sessionUri = Sessions.buildSessionUri(sessionId);
                final ContentValues values = querySessionDetails(sessionUri, resolver);
                final long localUpdated = values.getAsLong(SyncColumns.UPDATED);
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "found session " + entry.toString());
                    Log.v(TAG, "found localUpdated=" + localUpdated + ", server=" + serverUpdated);
		}
                if (localUpdated >= serverUpdated) continue;

                // Clear any existing values for this session, treating the
                // incoming details as authoritative.

		/* None of these are necessary; the delete keys are already unique and
		   ON CONFLICT REPLACE is in effect */
		/*
                batch.add(ContentProviderOperation.newDelete(sessionUri).build());
                batch.add(ContentProviderOperation.newDelete(sessionTracksUri).build());
                batch.add(ContentProviderOperation.newDelete(sessionSpeakersUri).build());
		*/

		// Change this value based on how many entries we have. Creates the right
		// number from the beginning to avoid resizing.
		final ContentValues newSession = new ContentValues(16);

                newSession.put(SyncColumns.UPDATED, serverUpdated);
                newSession.put(Sessions.SESSION_ID, sessionId);
                newSession.put(Sessions.SESSION_LEVEL, entry.get(Columns.SESSION_LEVEL));
                newSession.put(Sessions.SESSION_TITLE, entry.get(Columns.SESSION_TITLE));
                newSession.put(Sessions.SESSION_ABSTRACT, entry.get(Columns.SESSION_ABSTRACT));
                newSession.put(Sessions.SESSION_REQUIREMENTS, entry.get(Columns.SESSION_REQUIREMENTS));
                newSession.put(Sessions.SESSION_KEYWORDS, entry.get(Columns.SESSION_TAGS));
                newSession.put(Sessions.SESSION_HASHTAG, entry.get(Columns.SESSION_HASHTAG));
                newSession.put(Sessions.SESSION_SLUG, entry.get(Columns.SESSION_SLUG));
                newSession.put(Sessions.SESSION_URL, entry.get(Columns.SESSION_URL));
                newSession.put(Sessions.SESSION_MODERATOR_URL, entry.get(Columns.SESSION_MODERATOR_URL));
                newSession.put(Sessions.SESSION_YOUTUBE_URL, entry.get(Columns.SESSION_YOUTUBE_URL));
                newSession.put(Sessions.SESSION_PDF_URL, entry.get(Columns.SESSION_PDF_URL));
                newSession.put(Sessions.SESSION_FEEDBACK_URL, entry.get(Columns.SESSION_FEEDBACK_URL));
                newSession.put(Sessions.SESSION_NOTES_URL, entry.get(Columns.SESSION_NOTES_URL));

                // Inherit starred value from previous row
                if (values.containsKey(Sessions.SESSION_STARRED)) {
                    newSession.put(Sessions.SESSION_STARRED,
				   values.getAsInteger(Sessions.SESSION_STARRED));
                }

                // Parse time string from two columns, which is pretty ugly code
                // since it assumes the column format is "Wednesday May 19" and
                // "10:45am-11:45am". Future spreadsheets should use RFC 3339.
                final String start = entry.get(Columns.SESSION_START);
                final String end = entry.get(Columns.SESSION_END);

		Date startTime, endTime;
		try {
		    startTime = sTimeFormat.parse(start);
		} catch (java.text.ParseException e) { 
		    startTime = null;
		}
		if(startTime == null) {
		    try {
			// Google's spreadsheets don't report the time if it's
			// midnight, so we have to manually append it
			startTime = sTimeFormat.parse(start + " 0:00");
		    } catch (java.text.ParseException e) {
			// give up
			throw new HandlerException("Problem parsing timestamp", e);
		    }
		}

		try {
		    endTime = sTimeFormat.parse(end);
		} catch (java.text.ParseException e) { 
		    endTime = null;
		}
		if(endTime == null) {
		    try {
			// Google's spreadsheets don't report the time if it's
			// midnight, so we have to manually append it
			endTime = sTimeFormat.parse(end + " 0:00");
		    } catch (java.text.ParseException e) {
			// give up
			throw new HandlerException("Problem parsing timestamp", e);
		    }
		}

		final String title =
		    sTimeSpanFormat.format(startTime) +
		    " - " +
		    sTimeSpanFormat.format(endTime);
		    
                final String blockId = ParserUtils.findOrCreateBlock(
		        title,
                        ParserUtils.BLOCK_TYPE_SESSION,
                        startTime.getTime(), endTime.getTime(), batch, resolver);
                newSession.put(Sessions.BLOCK_ID, blockId);

                // Assign room
                final String roomId = sanitizeId(entry.get(Columns.SESSION_ROOM));
                newSession.put(Sessions.ROOM_ID, roomId);

                // Normal session details ready, write to provider
		bulkSessions.add(newSession);

                // Assign tracks
                final String[] tracks = splitComma(entry.get(Columns.SESSION_TRACK));
                for (String track : tracks) {
		    final ContentValues sesTrack = new ContentValues(2);
                    final String trackId = translateTrackIdAlias(sanitizeId(track));

		    sesTrack.put(SessionsTracks.SESSION_ID, sessionId);
		    sesTrack.put(SessionsTracks.TRACK_ID, trackId);
		    bulkSesTracks.add(sesTrack);
                }

                // Assign speakers
                final String[] speakers = splitComma(entry.get(Columns.SESSION_SPEAKERS));
                for (String speaker : speakers) {
		    final ContentValues sesSpkr = new ContentValues(2);
                    final String speakerId = sanitizeId(speaker, true);

		    sesSpkr.put(SessionsSpeakers.SESSION_ID, sessionId);
		    sesSpkr.put(SessionsSpeakers.SPEAKER_ID, speakerId);
		    bulkSesSpeakers.add(sesSpkr);
                }
            }
        }

	Log.d(TAG, "Bulk insert start");
	resolver.bulkInsert(Sessions.CONTENT_URI, bulkSessions.toArray(new ContentValues[0]));
	resolver.bulkInsert(sessionTracksUri, bulkSesTracks.toArray(new ContentValues[0]));
	resolver.bulkInsert(sessionSpeakersUri, bulkSesSpeakers.toArray(new ContentValues[0]));
	Log.d(TAG, "Bulk insert done");

        return batch;
    }

    /**
     * Parse the given date and time coming from spreadsheet. This is tightly
     * tied to a specific format. Ideally, if the source used use RFC 3339 we
     * could parse quickly using {@link Time#parse3339}.
     * <p>
     * Internally assumes PST time zone and year 2011.
     *
     * @param date String of format "Wednesday May 19", usually read from
     *            {@link Columns#SESSION_DATE}.
     * @param time String of format "10:45am", usually after splitting
     *            {@link Columns#SESSION_TIME}.
     */
    private static long parseTime(String date, String time) throws HandlerException {
        final String composed = String.format("%s %s", date, time);
        try {
            return sTimeFormat.parse(composed).getTime();
        } catch (java.text.ParseException e) {
            throw new HandlerException("Problem parsing timestamp", e);
        }
    }

    private static ContentValues querySessionDetails(Uri uri, ContentResolver resolver) {
        final ContentValues values = new ContentValues();
        final Cursor cursor = resolver.query(uri, SessionsQuery.PROJECTION, null, null, null);
        try {
            if (cursor.moveToFirst()) {
                values.put(SyncColumns.UPDATED, cursor.getLong(SessionsQuery.UPDATED));
                values.put(Sessions.SESSION_STARRED, cursor.getInt(SessionsQuery.STARRED));
            } else {
                values.put(SyncColumns.UPDATED, ScheduleContract.UPDATED_NEVER);
            }
        } finally {
            cursor.close();
        }
        return values;
    }

    private interface SessionsQuery {
        String[] PROJECTION = {
                Tables.SESSIONS+'.'+SyncColumns.UPDATED,
                Sessions.SESSION_STARRED,
        };

        int UPDATED = 0;
        int STARRED = 1;
    }

    /** Columns coming from remote spreadsheet. */
    private interface Columns {
        String SESSION_START = "sessionstart";
        String SESSION_END = "sessionend";
        String SESSION_ROOM = "sessionroom";
        String SESSION_TRACK = "sessiontrack";
        String SESSION_LEVEL = "sessionlevel";
        String SESSION_TITLE = "sessiontitle";
        String SESSION_TAGS = "sessiontags";
        String SESSION_HASHTAG = "sessionhashtag";
        String SESSION_SLUG = "sessionslug";
        String SESSION_SPEAKERS = "sessionspeakers";
        String SESSION_ABSTRACT = "sessionabstract";
        String SESSION_REQUIREMENTS = "sessionrequirements";
        String SESSION_URL = "sessionurl";
        String SESSION_MODERATOR_URL = "sessionmoderatorurl";
        String SESSION_YOUTUBE_URL = "sessionyoutubeurl";
        String SESSION_PDF_URL = "sessionpdfurl";
        String SESSION_FEEDBACK_URL = "sessionfeedbackurl";
        String SESSION_NOTES_URL = "sessionnotesurl";

        // session_date: Wednesday May 19
        // session_time: 10:45am-11:45am
        // session_room: 6
        // session_track: Enterprise, App Engine
        // session_level: 201
        // session_title: Run corporate applications on Google App Engine?  Yes we do.
        // session_slug: run-corporate-applications
        // session_tags: Enterprise, SaaS, PaaS, Hosting, App Engine, Java
        // session_speakers: Ben Fried, John Smith
        // session_abstract: And you can too! Come hear Google's CIO Ben Fried describe...
        // session_requirements: None
        // session_url: http://www.google.com/events/io/2011/foo
        // session_hashtag: #io11android1
        // session_youtube_url
        // session_pdf_url
        // session_feedback_url
        // session_moderator_url
        // session_notes_url
    }
}
